// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-07-17

package src

import (
	"context"
	"fmt"
	"gitee.com/go-libs/log"
	gr "github.com/go-redis/redis/v8"
	"math/rand"
	"time"
)

const (
	DefaultLockDuration = time.Second * 60
	DefaultLockRenew    = 25
	DefaultLockPrefix   = "__LOCKER__"
)

// Lock
// apply a distributed lock resource. Return nil if given key applied by other connection.
func Lock(ctx context.Context, key string, timeouts ...int) (Locker, error) {
	var (
		cmd *gr.BoolCmd
		err error
		res = &locker{
			ctx:      ctx,
			duration: DefaultLockDuration,
			key:      fmt.Sprintf(`%s:%s`, DefaultLockPrefix, key),
		}
		yes bool
	)

	// Acquire
	// a connection from redis pool.
	if res.connection, err = Connector.GetConnection(ctx); err != nil {
		log.Errorfc(ctx, `[redis][lock] failed to apply for distributed lock: key="%s", error="%v"`, res.key, err)
		return nil, err
	}

	// Override default duration.
	if len(timeouts) > 0 && timeouts[0] > 0 {
		res.duration = time.Duration(timeouts[0]) * time.Second
	}

	// Build a random integer value for distributed lock resource.
	res.value = rand.Int31()

	// Send redis command.
	cmd = res.connection.SetNX(ctx, res.key, res.value, res.duration)

	// Compare bool result.
	if yes, err = cmd.Result(); err != nil {
		log.Errorfc(ctx, `[redis][lock] failed to apply for distributed lock: key="%s", error="%v"`, res.key, err)
		return nil, err
	}

	// Succeed to apply.
	if yes {
		log.Infofc(ctx, `[redis][lock] succeed to apply for distributed lock: key="%s", value="%v", timeout="%vs"`, res.key, res.value, res.duration.Seconds())
		return res, nil
	}

	// Failed to apply for exists, It's applied by other goroutine.
	log.Infofc(ctx, `[redis][lock] failed to apply for distributed lock: key="%s", error="exists"`, res.key)

	// Close acquired connection if failed to apply.
	if err = res.connection.Close(); err != nil {
		log.Warnfc(ctx, `[redis][lock] failed to close connection: key="%s", error="%v"`, res.key, err)
	}
	return nil, nil
}
